/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.phoenix.repository;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.jcr.AccessDeniedException;
import javax.jcr.Credentials;
import javax.jcr.InvalidItemStateException;
import javax.jcr.Item;
import javax.jcr.ItemExistsException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.LoginException;
import javax.jcr.NoSuchWorkspaceException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
import javax.jcr.ReferentialIntegrityException;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.ValueFactory;
import javax.jcr.Workspace;
import javax.jcr.lock.LockException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.NoSuchNodeTypeException;
import javax.jcr.observation.Event;
import javax.jcr.version.VersionException;

import net.phoenix.repository.dao.JdbcDao;
import net.phoenix.repository.dao.NodeTableQueryDao;
import net.phoenix.repository.data.NodeData;
import net.phoenix.repository.observation.BasicObservationManager;
import net.phoenix.repository.query.xpath.JCRNavigator;
import net.phoenix.repository.query.xpath.JCRPath;
import net.phoenix.repository.query.xpath.XPathQuery;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jaxen.JaxenException;
import org.jaxen.JaxenHandler;
import org.jaxen.XPath;
import org.jaxen.expr.DefaultXPathFactory;
import org.jaxen.expr.LocationPath;
import org.jaxen.expr.NameStep;
import org.jaxen.expr.Step;
import org.jaxen.expr.XPathExpr;
import org.jaxen.expr.XPathFactory;
import org.jaxen.saxpath.SAXPathException;
import org.jaxen.saxpath.XPathReader;
import org.jaxen.saxpath.helpers.XPathReaderFactory;

/**
 * Jigsaw server
 * 
 * @author <a href="shamphone@gmail.com">Li XiongFeng</a>
 * @date 2012-1-2
 * @version 1.0.0
 */
public abstract class JdbcSession extends NamespacedSession {
	private static final Log log = LogFactory.getLog(JdbcSession.class);
	private BasicRepository repository;
	private Node root;
	private Workspace workspace;
	private JCRNavigator navigator;
	private boolean login;
	private Map<String, BasicNode> pending;
	private Connection connection;
	private static final String ROOT_ID = "1001";

	public JdbcSession(BasicRepository repository, Connection connection,
			String workspaceName) throws LoginException,
			NoSuchWorkspaceException, RepositoryException {
		super(repository.createNamespaceRegistry());
		this.repository = repository;
		this.navigator = new JCRNavigator(this);
		this.workspace = new BasicWorkspace(this.repository, this,
				workspaceName);
		this.pending = new LinkedHashMap<String, BasicNode>();
		this.root = null;
		this.login = true;
		this.connection = connection;
	}

	@Override
	public ValueFactory getValueFactory()
			throws UnsupportedRepositoryOperationException, RepositoryException {
		return this.repository.getValueFactory();
	}

	/**
	 * add a pending node to the list;
	 * 
	 * @param node
	 */
	protected void addPending(BasicNode node) {
		if (!this.pending.values().contains(node))
			this.pending.put(node.getIdentifier(), node);
	}

	protected void removePending(BasicNode node) {
		this.pending.remove(node.getIdentifier());
	}

	@Override
	public void refresh(boolean keepChanges) throws RepositoryException {
		if (!keepChanges)
			this.pending.clear();

	}

	@Override
	public boolean hasPendingChanges() throws RepositoryException {
		return this.pending.size() > 0;
	}

	@Override
	public Session impersonate(Credentials credentials) throws LoginException,
			RepositoryException {
		Session session = repository.login(credentials,
				this.workspace.getName());
		return session;
	}

	@Override
	public Repository getRepository() {
		return repository;
	}

	@Override
	public void logout() {
		this.login = false;
		this.workspace = null;
		try {
			this.connection.commit();
		} catch (SQLException ex) {
			try {
				this.connection.rollback();
			} catch (SQLException e) {
				log.error("Error in rollback. ", e);
			}
		} finally {
			try {
				this.connection.close();
			} catch (SQLException e) {
				log.error("Error in close connection. ", e);
			}
			this.connection = null;
		}
	}

	@Override
	public boolean isLive() {
		return login;
	}

	@Override
	public Workspace getWorkspace() {
		return workspace;
	}

	@Override
	public String getUserID() {
		try {
			return this.connection.getMetaData().getUserName();
		} catch (SQLException ex) {
			throw new RepositoryRuntimeException(ex);
		}
	}

	/**
	 * 
	 * @param nodeId
	 *            String
	 * @return Node 该方法首先在缓存中查找node，如果不能找到，则通过Dao文件在数据库中查找
	 * @throws RepositoryException
	 */
	@Override
	public Node getNodeByIdentifier(final String nodeId)
			throws RepositoryException {
		if (nodeId == null) {
			throw new IllegalArgumentException("nodeId should not be null.");
		}
		// read from pending list first.
		Node node = this.pending.get(nodeId);
		if (node != null)
			return node;
		NodeTableQueryDao dao = this.getDao(NodeTableQueryDao.class);
		NodeData data;
		try {
			data = dao.findByID(nodeId);
		} catch (SQLException e) {
			throw new RepositoryException(e);
		}
		if (data != null)
			node = new BasicNode(JdbcSession.this, data);

		if (node != null)
			return node;

		throw new ItemNotFoundException(nodeId);

	}

	/**
	 * 获取根结点
	 * 
	 * @return Node
	 * @throws RepositoryException
	 * @throws PathNotFoundException
	 */
	@Override
	public Node getRootNode() throws PathNotFoundException, RepositoryException {
		if (this.root == null)
			this.root = this.getNodeByIdentifier(ROOT_ID);
		return this.root;
	}

	@Override
	public Item getItem(String absPath) throws PathNotFoundException,
			RepositoryException {
		XPath xpath;
		try {
			xpath = new JCRPath(absPath, this.navigator);

		} catch (JaxenException e) {
			throw new PathNotFoundException(absPath);
		}
		try {
			return (Item) xpath.selectSingleNode(this.getRootNode());
		} catch (JaxenException e) {
			throw new RepositoryException(e);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.jcr.Session#getNode(java.lang.String)
	 */
	@Override
	public Node getNode(String absPath) throws PathNotFoundException,
			RepositoryException {
		XPathQuery path = new XPathQuery();
		path.init(this.getWorkspace(), absPath);
		NodeIterator nodes = path.execute().getNodes();
		if (nodes.hasNext())
			return nodes.nextNode();
		throw new PathNotFoundException(absPath);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.jcr.Session#getProperty(java.lang.String)
	 */
	@Override
	public Property getProperty(String absPath) throws PathNotFoundException,
			RepositoryException {
		Item item = this.getItem(absPath);
		if (!item.isNode())
			return (Property) item;
		throw new PathNotFoundException(absPath);
	}

	@Override
	public void save() throws AccessDeniedException, ItemExistsException,
			ReferentialIntegrityException, ConstraintViolationException,
			InvalidItemStateException, VersionException, LockException,
			NoSuchNodeTypeException, RepositoryException {
		final BasicObservationManager manager = (BasicObservationManager) this
				.getWorkspace().getObservationManager();
		for (BasicNode node : pending.values()) {
			node.save();
			manager.fire(node, Event.NODE_ADDED);
		}
		pending.clear();
	}

	@Override
	public boolean itemExists(String absPath) throws RepositoryException {
		return this.getItem(absPath) != null;
	}

	@Override
	public boolean nodeExists(String absPath) throws RepositoryException {
		return this.getNode(absPath) != null;
	}

	@Override
	public boolean propertyExists(String absPath) throws RepositoryException {
		return this.getProperty(absPath) != null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.jcr.Session#move(java.lang.String, java.lang.String)
	 */
	@Override
	public void move(String srcAbsPath, String destAbsPath)
			throws ItemExistsException, PathNotFoundException,
			VersionException, ConstraintViolationException, LockException,
			RepositoryException {
		Node node = this.getNode(srcAbsPath);
		try {
			this.getNode(destAbsPath);
			throw new ItemExistsException(destAbsPath);
		} catch (PathNotFoundException ex) {
			// path not fould, is OK.
		}

		XPathFactory factory = new DefaultXPathFactory();
		LocationPath parentPath = null;
		String name = null;
		try {
			XPathReader reader = XPathReaderFactory.createReader();
			JaxenHandler handler = new JaxenHandler();
			reader.setXPathHandler(handler);
			reader.parse(destAbsPath);

			XPathExpr expr = handler.getXPathExpr();
			List<?> steps = ((LocationPath) expr.getRootExpr()).getSteps();

			parentPath = factory.createAbsoluteLocationPath();
			for (int i = 0; i < steps.size() - 1; i++) {
				Step step = (Step) steps.get(i);
				parentPath.addStep(step);
			}

			NameStep last = ((NameStep) steps.get(steps.size() - 1));
			last.simplify();
			name = last.getLocalName();
		} catch (SAXPathException ex) {
			throw new PathNotFoundException(ex);
		}

		String parentExpr = parentPath.simplify().getText();
		Node parent = this.getNode(parentExpr);
		if (parent == null)
			throw new PathNotFoundException(parentExpr);
		((BasicNode) node).setParent(parent, name);
		((BasicNode) node).save();

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.jcr.Session#removeItem(java.lang.String)
	 */
	@Override
	public void removeItem(String absPath) throws VersionException,
			LockException, ConstraintViolationException, AccessDeniedException,
			RepositoryException {
		Item item = this.getItem(absPath);
		if (item.isNode()) {
			Node node = (Node) item;
			node.remove();
		} else {
			Property property = (Property) item;
			property.remove();
		}
	}

	public <T extends JdbcDao> T getDao(Class<T> clazz) {
		return this.repository.getJdbcDaoFactory().getJdbcDao(clazz,
				this.connection);
	}

}
